package com.mlming.simpledb.backend.dm;

import com.mlming.simpledb.backend.common.AbstractCache;
import com.mlming.simpledb.backend.dm.dataItem.DataItem;
import com.mlming.simpledb.backend.dm.dataItem.DataItemImpl;
import com.mlming.simpledb.backend.dm.logger.Logger;
import com.mlming.simpledb.backend.dm.page.Page;
import com.mlming.simpledb.backend.dm.page.PageOneDataHandler;
import com.mlming.simpledb.backend.dm.page.PageXDataHandler;
import com.mlming.simpledb.backend.dm.pageCache.PageCache;
import com.mlming.simpledb.backend.dm.pageIndex.PageIndex;
import com.mlming.simpledb.backend.dm.pageIndex.PageInfo;
import com.mlming.simpledb.backend.tm.TransactionManager;
import com.mlming.simpledb.backend.utils.Panic;
import com.mlming.simpledb.backend.utils.Types;
import com.mlming.simpledb.common.Error;

public class DataManagerImpl extends AbstractCache<DataItem> implements DataManager {

    TransactionManager tm; // TM模块的类
    PageCache pc; // 页面缓存类
    Logger logger; // 日志类
    PageIndex pIndex; // 页面索引类
    Page pageOne; // db文件的第一页

    public DataManagerImpl(PageCache pc, Logger logger, TransactionManager tm) {
        // 此时我们令maxResource = 0, 那么就不会满足get方法中的if(maxResource > 0) , 所以就不会进入到OOM的判断
        // 所以说: **maxResource = 0并不是代表着不缓存, 恰恰相反, 这是代表着缓存没有上限**
        super(0);

        this.pc = pc;
        this.logger = logger;
        this.tm = tm;
        this.pIndex = new PageIndex();
    }

    @Override
    /**
     * 当DataItem不在缓存里时, 会被调用该函数
     * 由于DataItem是存在页里面的, 所以我们先去PageCache里面获取页
     * 根据页面缓存机制, 如果页不存在缓存中, 会到db文件中去读取
     * 而只要页读到了缓存中, 自然而然DataItem也在缓存中了
     * 我们只需要对DataItem进行封装即可
     */
    protected DataItem getForCache(long uid) throws Exception {
        // 因为uid是DataItem的pgno和offset拼接而成的, 所以通过位运算取出其中的pgno, offset
        short offset = (short)(uid & ((1L << 16) - 1));
        uid >>>= 32;
        int pgno = (int)(uid & ((1L << 32) - 1));
        // 从PageCache中读取出页面(如果页面不存在缓存中,会到db文件去读取)
        Page pg = pc.getPage(pgno);
        // 包装为DataItem对象返回
        return DataItem.parseDataItem(pg, offset, this);
    }

    @Override
    /**
     * 当资源引用 == 0时, 说明该资源被驱逐了, 需要刷盘了
     * 而由于DataItem是存在页里面的, 要把当前DataItem刷回盘中,
     * 根据 "页是基本存储单位"的原则, 我们直接把该页release一下即可
     */
    protected void releaseForCache(DataItem dataItem) {
        dataItem.page().release();
    }

    @Override
    // 从pgno对应的页的offset位置读取出DataItem
    // 既然我们DataManagerImpl同时是DataItem的缓存, 那么对一个DataItem的读取, 必然是通过缓存机制去读取的
    public DataItem read(int pgno, short offset) throws Exception {
        // 1) 获得dataItem的uid
        long uid = DataItem.getUid(pgno, offset);
        // 2) 通过缓存机制来获得dataItem
        DataItem dataItem = super.get(uid);
        // 3) 判断该dataItem是否是不合法的, 如果是不合法的说明已经被删除, 则返回null, 并去除一个引用
        if(!dataItem.isValid()) {
            dataItem.release();
            return null;
        }
        return dataItem;
    }

    @Override
    // xid对应的事务 要往 数据库中插入data数据, 并返回新的dataItem的uid
    public long insert(long xid, byte[] data) throws Exception {
        // 1) 先把data包裹为一个DataItem的格式的数据: [ValidFlag] [DataSize] [Data]
        byte[] raw = DataItem.wrapDataItemRaw(data);
        if(raw.length > PageXDataHandler.MAX_FREE_SPACE) { // 如果当前raw大于数据页最大空闲空间, 则报错
            throw Error.DataTooLargeException;
        }
        // 2) 不断尝试获得可用页
        PageInfo pageInfo = null;
        for (int i = 0; i < 5; i++) {
            // 先通过页面索引, 获得一个可以满足插入空间的页面
            pageInfo = pIndex.select(raw.length);
            // 如果是null, 通过select的逻辑可以得知, 说明此时没有一个已存在的页可以存的下
            // 所以我们要开辟一个新页, 同时把该页放入到索引中, **再次尝试**
            if(pageInfo == null) {
                int newPgno = pc.newPage(PageXDataHandler.InitData());
                // 因为一开始页面没有数据, 所以空闲空间是MAX_FREE_SPACE
                pIndex.add(newPgno, PageXDataHandler.MAX_FREE_SPACE);
            } else {
                break;
            }
        }
        // 如果尝试了这么多次还是null, 说明可能是数据库的问题, 直接报错
        if(pageInfo == null) {
            throw Error.DatabaseBusyException;
        }
        // 3) 通过PageInfo获得pgno, 通过PageCache获得对应的Page,
        //    通过PageXDataHandler这个普通数据页管理类, 来实现往页面中插入数据
        Page pg = null;
        try {
            // 从PageCache中获得Page
            pg = pc.getPage(pageInfo.pgno);
            // 往日志里插入一条插入日志
            byte[] log = Recover.insertLog(xid, pg, raw);
            logger.log(log);
            // 通过PageXDataHandler这个普通数据页管理类, 来实现往页面中插入数据
            short offset = PageXDataHandler.insert(pg, raw);
            // 此处page用完了, 要release一下, 因为上面getPage会引用+1
            pg.release();
            // 4) 返回该dataItem的uid
            return DataItem.getUid(pageInfo.pgno,offset);
        } finally {
            // 无论上面的操作是否成功, 都必须将取出的pg重新插入pIndex
            if(pg != null) {
                // 如果不是null ,说明上述操作成功, 那么就要重新计算一下其freeSpace,然后插入索引中
                pIndex.add(pageInfo.pgno, PageXDataHandler.getFreeSpace(pg));
            } else {
                // 如果是null, 说明上述操作不成功, 则把原来的数据重新插入到索引中
                pIndex.add(pageInfo.pgno, pageInfo.freeSpace);
            }
        }
    }

    @Override
    public void close() {
        super.close();

        logger.close();
        // 数据库关闭, 则要对第一页写入和之前写入的相同的字符串, 方便之后判断是否正常关闭
        PageOneDataHandler.setVcClose(pageOne);
        pageOne.release();
        pc.close();
    }

    // 释放缓存中的dataItem的一个引用
    public void release(DataItem dataItem) {
        super.release(dataItem.getUid());
    }

    // 为xid生成update日志
    public void logDataItem(long xid, DataItem di) {
        byte[] log = Recover.updateLog(xid, di);
        logger.log(log);
    }

    // 在创建文件时初始化PageOne, 用于create静态方法
    void initPageOne() {
        // 在db文件中新建一个第一页PageOne, 并获得其pgno
        int pgno = pc.newPage(PageOneDataHandler.InitData());
        assert pgno == 1; // 断言判断pgno == 1, 看看是不是第一页
        try {
            // 把该第一页放入缓存中, 同时存到pageOne变量中
            pageOne = pc.getPage(pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
    }

    // 在打开已有文件时时读入PageOne，并验证正确性, 用于open静态方法
    boolean loadCheckPageOne() {
        try {
            // 把该第一页放入缓存中, 同时存到pageOne变量中
            pageOne = pc.getPage(1);
        } catch (Exception e) {
            Panic.panic(e);
        }
        // 通过PageOneDataHandler检查其正确性
        return PageOneDataHandler.checkVc(pageOne);
    }

    // 初始化pageIndex, 把所有的页面都放入到页面索引中去
    void fillPageIndex() {
        int pageNumber = pc.getPageNumber(); // 获得当前最大页号
        // 因为pageNo是从1开始算的, 而1是第一页检验页, 后面的才是数据页, 所以从2开始遍历
        for(int i = 2; i <= pageNumber; i ++) {
            Page pg = null;
            try {
                pg = pc.getPage(i);
            } catch (Exception e) {
                Panic.panic(e);
            }
            // 将页面插入到索引中
            pIndex.add(pg.getPageNumber(), PageXDataHandler.getFreeSpace(pg));

            pg.release();
        }
    }
}
